/*
 * Copyright (C) 2013 Sony Computer Entertainment Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY SONY COMPUTER ENTERTAINMENT INC. ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL SONY COMPUTER ENTERTAINMENT INC.
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"

#include "InspectorServerManx.h"

#include "DocumentLoader.h"
#include "FileSystem.h"
#include "Frame.h"
#include "FrameLoader.h"
#include "HTTPRequest.h"
#include "InspectorClientManx.h"
#include "InspectorProtocolVersion.h"
#include "InspectorValues.h"
#include "MIMETypeRegistry.h"
#include "Page.h"
#include "WebKitVersion.h"
#include "WebSocketServerConnection.h"
#include "WebViewPrivate.h"

#include <errno.h>
#include <manx/NetworkProfile.h>
#include <wtf/Assertions.h>
#include <wtf/text/CString.h>

using namespace WebKit;

namespace {
    // Use the last revision before the Blink/WebKit fork as our reference revision
    // (cf. https://groups.google.com/a/chromium.org/d/msg/blink-dev/J41PSKuMan0/gD5xcqicqP8J)
    // Revision numbering was maintained in Blink, but since history has started diverging after
    // that commit the WebKit revisions aren't mappable anymore.
    // ChromeDriver adjusts some event handling depending on that revision number.
    // (grep 'blink_revision' in chromedriver/chrome/navigation_tracker.cc)
    const int webkitRevision = 147503;
}

namespace WebCore {

static unsigned clientIdFromRequestPath(const String& path)
{
    size_t start = path.reverseFind('/');
    String numberString = path.substring(start + 1, path.length() - start - 1);

    bool ok = false;
    unsigned number = numberString.toUIntStrict(&ok);
    if (!ok)
        return 0;
    return number;
}

InspectorServerManx& InspectorServerManx::shared()
{
    static InspectorServerManx& server = *new InspectorServerManx;
    return server;
}

InspectorServerManx::InspectorServerManx()
    : WebSocketServer(this)
    , m_nextAvailableClientId(1)
{
}

InspectorServerManx::~InspectorServerManx()
{
    // Close any remaining open connections.
    HashMap<unsigned, WebSocketServerConnection*>::iterator end = m_connectionMap.end();
    for (HashMap<unsigned, WebSocketServerConnection*>::iterator it = m_connectionMap.begin(); it != end; ++it) {
        WebSocketServerConnection* connection = it->value;
        InspectorClientManx* client = m_clientMap.get(connection->identifier());
        closeConnection(client, connection);
    }
}

int InspectorServerManx::registerClient(InspectorClientManx* client)
{
#ifndef ASSERT_DISABLED
    for (auto it = m_clientMap.begin(); it != m_clientMap.end(); ++it)
        ASSERT(it->second != client);
#endif

    int clientId = m_nextAvailableClientId++;
    m_clientMap.set(clientId, client);

    updateServerState();

    return clientId;
}

void InspectorServerManx::unregisterClient(int clientId)
{
    m_clientMap.remove(clientId);
    WebSocketServerConnection* connection = m_connectionMap.get(clientId);
    if (connection)
        closeConnection(0, connection);

    updateServerState();
}

void InspectorServerManx::sendMessageOverConnection(unsigned clientIdForConnection, const String& message)
{
    WebSocketServerConnection* connection = m_connectionMap.get(clientIdForConnection);
    if (connection)
        connection->sendWebSocketMessage(message);
}

void InspectorServerManx::didReceiveUnrecognizedHTTPRequest(WebSocketServerConnection* connection, PassRefPtr<HTTPRequest> request)
{
    // request->url() contains only the path extracted from the HTTP request line
    // and KURL is poor at parsing incomplete URLs, so extract the interesting parts manually.
    String path = request->url();
    size_t pathEnd = path.find('?');
    if (pathEnd == notFound)
        pathEnd = path.find('#');
    if (pathEnd != notFound)
        path.truncate(pathEnd);

    // Ask for the complete payload in memory for the sake of simplicity. A more efficient way would be
    // to ask for header data and then let the platform abstraction write the payload straight on the connection.
    Vector<char> body;
    String contentType;
    bool found = platformResourceForPath(path, body, contentType);

    HTTPHeaderMap headerFields;
    headerFields.set("Connection", "close");
    headerFields.set("Content-Length", String::number(body.size()));
    if (found)
        headerFields.set("Content-Type", contentType);

    // Send when ready and close immediately afterwards.
    connection->sendHTTPResponseHeader(found ? 200 : 404, found ? "OK" : "Not Found", headerFields);
    connection->sendRawData(body.data(), body.size());
    connection->shutdownAfterSendOrNow();
}

bool InspectorServerManx::didReceiveWebSocketUpgradeHTTPRequest(WebSocketServerConnection*, PassRefPtr<HTTPRequest> request)
{
    String path = request->url();

    // NOTE: Keep this in sync with WebCore/inspector/front-end/inspector.js.
    DEFINE_STATIC_LOCAL(const String, inspectorWebSocketConnectionPathPrefix, ("/devtools/page/"));

    // Unknown path requested.
    if (!path.startsWith(inspectorWebSocketConnectionPathPrefix))
        return false;

    int clientId = clientIdFromRequestPath(path);
    // Invalid client id.
    if (!clientId)
        return false;

    // There is no client for that client id.
    InspectorClientManx* client = m_clientMap.get(clientId);
    if (!client)
        return false;

    return true;
}

void InspectorServerManx::didEstablishWebSocketConnection(WebSocketServerConnection* connection, PassRefPtr<HTTPRequest> request)
{
    String path = request->url();
    unsigned clientId = clientIdFromRequestPath(path);
    ASSERT(clientId);

    // Ignore connections to a page that already have a remote inspector connected.
    if (m_connectionMap.contains(clientId)) {
        LOG_ERROR("A remote inspector connection already exist for client ID %d. Ignoring.", clientId);
        connection->shutdownNow();
        return;
    }

    // Map the clientId to the connection in case we need to close the connection locally.
    connection->setIdentifier(clientId);
    m_connectionMap.set(clientId, connection);

    InspectorClientManx* client = m_clientMap.get(clientId);
    client->remoteFrontendConnected();
}

void InspectorServerManx::didReceiveWebSocketMessage(WebSocketServerConnection* connection, const String& message)
{
    // Dispatch incoming remote message locally.
    unsigned clientId = connection->identifier();
    ASSERT(clientId);
    InspectorClientManx* client = m_clientMap.get(clientId);
    client->dispatchMessageFromRemoteFrontend(message);
}

void InspectorServerManx::didCloseWebSocketConnection(WebSocketServerConnection* connection)
{
    // Connection has already shut down.
    unsigned clientId = connection->identifier();
    if (!clientId)
        return;

    // The socket closing means the remote side has caused the close.
    InspectorClientManx* client = m_clientMap.get(clientId);
    closeConnection(client, connection);
}

String InspectorServerManx::inspectorFrontendPath() const
{
    return inspectorBasePath() + String("/inspector.html");
}

String InspectorServerManx::inspectorBasePath() const
{
    return getenv("MANX_INSPECTOR_SERVER_PATH");
}

bool InspectorServerManx::platformResourceForPath(const String& path, Vector<char>& data, String& contentType)
{
    if (!path.startsWith('/'))
        return false;

    if (m_clientMap.isEmpty())
        return false;

    if (path.startsWith("/json")) {
        const String jsonSubpath = path.substring(5);

        if (jsonSubpath.isEmpty()) {
            RefPtr<InspectorArray> message = InspectorArray::create();

            for (HashMap<unsigned, InspectorClientManx*>::const_iterator it = m_clientMap.begin(); it != m_clientMap.end(); ++it) {
                InspectorClientManx* client = it->value;
                RefPtr<InspectorObject> tabInfo = InspectorObject::create();

                tabInfo->setString("id", String::number(it->key));
                if (client->page()->mainFrame() && client->page()->mainFrame()->loader()->activeDocumentLoader()) {
                    tabInfo->setString("title", client->page()->mainFrame()->loader()->activeDocumentLoader()->title().string());
                    tabInfo->setString("url", client->page()->mainFrame()->loader()->activeDocumentLoader()->url());
                }
                tabInfo->setString("type", "page");
                tabInfo->setString("webSocketDebuggerUrl", String::format("ws://%s:%u/devtools/page/%u", bindAddress().latin1().data(), port(), it->key));

                char* envValue = getenv("MANX_INSPECTOR_SERVER_PROCESS_NAME");
                if (envValue)
                    tabInfo->setString("processDisplayName", envValue);

                message->pushObject(tabInfo);
            }

            CString tabInfo = message->toJSONString().utf8();
            data.append(tabInfo.data(), tabInfo.length());
        } else if (jsonSubpath == "/version") {
            InspectorClientManx* client = m_clientMap.begin()->value;
            ASSERT(client);

            // Basic version info needed by the WebDriver server
            RefPtr<InspectorObject> versionInfo = InspectorObject::create();
            versionInfo->setString("Browser", ""); // Intentionally kept blank, indicates custom browser to ChromeDriver
            versionInfo->setString("Protocol-Version", inspectorProtocolVersion());
            versionInfo->setString("User-Agent", client->webview()->userAgent());
            versionInfo->setString("WebKit-Version", String::format("%d.%d (@%d)", WEBKIT_MAJOR_VERSION, WEBKIT_MINOR_VERSION, webkitRevision));

            CString versionInfoString = versionInfo->toJSONString().utf8();
            data.append(versionInfoString.data(), versionInfoString.length());
        } else {
            // Unsupported command
            return false;
        }

        contentType = "application/json; charset=utf-8";
        return true;
    }

    // Point the default path to display the inspector landing page.
    // All other paths are mapped directly to a resource, if possible.
    const String localPath = (path == "/") ? inspectorFrontendPath() : inspectorBasePath() + path;

    PlatformFileHandle handle = openFile(localPath, OpenForRead);
    if (!isHandleValid(handle)) {
        LOG_ERROR("WebInspectorServer: couldn't access platform resource '%s' for reading! (%d)", localPath.utf8().data(), errno);
        return false;
    }

    long long fileSize;
    if (!getFileSize(localPath, fileSize)) {
        LOG_ERROR("WebInspectorServer: couldn't get file size for '%s'! (%d)", localPath.utf8().data(), errno);
        closeFile(handle);
        return false;
    }
    data.grow(fileSize);
    if (readFromFile(handle, data.data(), data.size()) < fileSize) {
        LOG_ERROR("WebInspectorServer: didn't read all contents of file '%s'! (%d)", localPath.utf8().data(), errno);
        closeFile(handle);
        return false;
    }
    closeFile(handle);

    contentType = MIMETypeRegistry::getMIMETypeForPath(localPath);

    return true;
}

void InspectorServerManx::closeConnection(InspectorClientManx* client, WebSocketServerConnection* connection)
{
    // Local side cleanup.
    if (client)
        client->remoteFrontendDisconnected();

    // Remote side cleanup.
    m_connectionMap.remove(connection->identifier());
    connection->setIdentifier(0);
    connection->shutdownNow();
}

void InspectorServerManx::updateServerState()
{
    char* envValue = getenv("MANX_INSPECTOR_SERVER_PORT");
    if (!envValue)
        return;

    if (!m_clientMap.isEmpty() && serverState() == Closed) {
        const int inspectorServerPort = atoi(envValue);
        if (inspectorServerPort <= 0) {
            LOG_ERROR("Invalid Inspector Server port!");
            return;
        }

        if (!listen(Manx::NetworkProfile::ipAddress(), inspectorServerPort))
            LOG_ERROR("Couldn't start the Inspector Server!");

    } else if (m_clientMap.isEmpty() && serverState() == Listening) {
        close();
        ASSERT(serverState() == Closed);
    }
}

}
